/*

    Silobox 2 - JavaScript performance benchmarking
	by Mat Ryer
	
	with Simon Howard
	
	Homepage:
	http://silobox.com/

	Project homepage:
	http://silobox.googlecode.com/
	

  	Permission is hereby granted, free of charge, to any person obtaining
	a copy of this software and associated documentation files (the
	"Software"), to deal in the Software without restriction, including
	without limitation the rights to use, copy, modify, merge, publish,
	distribute, sublicense, and/or sell copies of the Software, and to
	permit persons to whom the Software is furnished to do so, subject to
	the following conditions:

	The above copyright notice and this permission notice shall be
	included in all copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
	EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
	LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
	OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
	WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
	
	
	
	File outline
	------------------------------------------------------------------------

	WORKFLOW
	
		Contains methods that control the flow of the Silobox process
	
	
	URL
	
		Contains methods that deal with URLs including understanding 
		the querystring and redirection.
	
	
	DOM
	
		Contains methods that deal with controling the DOM including
		updating HTML and loading scripts.
			
	
	BENCHMARKING
	
		Contains methods that perform actual benchmarking work
	
	
	VIEWS
	
		Contains methods that update the view of the document including
		displaying project details and results
	
	
	HELPER METHODS
	
		Contians methods that can be used in tests to perform
		common tasks
	
	
	BROWSER DETECTION
	
		Detects the browser the user is running the tests on
	
*/

// defaults
var sbdefaultreps = 100;
var sbdefaultiterations = 1000;
var sbdefaulttestdelay = 5000;
var sbdefaulttitle = "<< no title >>";

// states
var sbstart = 0; // nothing has happened yet
var sbviewproj = 1; // viewing the project
var sbrunning = 2; // running the tests
var sbresults = 3; // showing the results
var sbargs = null; // stores arguments
var sbcurrentstate = -1; // stores current state
var sbproject = null; // stores the project settings

/*******************************************************************************************

	WORKFLOW

*******************************************************************************************/

// page load
//
// 		loads the project file
//		and waits for execution to continue through the silobox() method
//
var sbmain = function() {

    var a = sbargs = sbdeserialize(sbquery());
    var s = sbcurrentstate = sbstate(sbargs);

    if (s == sbstart) {

        // request the project file
        sbshow_request_project();

    } else {

        // we have the project file - so load it
        sbloadproject(a.p);

    }

};

// loads the project
var sbloadproject = function(p) {

    sbstatus("Loading project " + p);

    sbscript(p);

    // ENHANCEMENT: add a timeout now to check to ensure the project file
    // was loaded correctly and report an error if it didn't.
    
};

/*
	User Function
	
	Called by the users scripts to tell the engine what to do
	
	USAGE
	
		silobox( project )
			Sets the project settings and triggers next steps
		
*/
var silobox = function(o) {

    // this is the project being loaded
    sbproject = sbsetprojectdefaults(o);

    switch (sbcurrentstate) {

	     case sbviewproj:

		    // show the project summary
		    sbshow_project_summary(sbproject);	
		    
		    // and the project details
			sbshow_project_details(sbproject, sbargs);
	
			break;
	
	     case sbrunning:
			
			var tests = sbproject.tests;
			var test = tests[sbargs.s - 1];
			sbstatus(sbargs.s + " of " + tests.length + " - Benchmarking test '" + test.title + "'");
			sbtitle(sbargs.s + " of " + tests.length + " - " + test.title);
			
			// schedule the test run after a short delay
			sbscheduleruntest(sbproject, sbargs);
	
			break;
	
	     case sbresults:
	
		    // show the project summary
		    sbshow_project_summary(sbproject);	
	
			// and the details
			//sbshow_project_details(sbproject, sbargs);
			
			// and the result
			sbshow_results(sbproject, sbargs);
	
			break;
	
	     default:
	         throw "Silobox: Invalid state";
	         break;

    }

};

// sets default values on a project
var sbsetprojectdefaults = function(p) {

    p.reps = p.reps || sbdefaultreps;
    p.iterations = p.iterations || sbdefaultiterations;
	p.testdelay = p.testdelay || sbdefaulttestdelay;
	p.title = p.title || sbdefaulttitle;
    p.tests = p.tests || [];

    return p;

};

// gets the application state given the current 'arguments'
var sbstate = function(o) {

    // no project file specified -
    if (!o.p) {

        // we have only just begun...
        return sbstart;

    } else {

        // no step - we haven't started yet
        if (!o.s) {

            // do we have any results?
            if (o.r) {

                // yes - display results
                return sbresults;

            } else {

                // no - we are viewing the project
                return sbviewproj;

            }

        } else {

            // we have a step - we are running
            return sbrunning;

        }

    }

};

var sbbegin = function(start_tests) {

	// get the file browser value
	var proj_name = sbget("projcet-file-path").value;
	
	// redirect the page
	var query = "p=" + escape(proj_name);
	
	// if 'begin' is true, start straight away
	if (start_tests) {
		query += "&s=1";
	};
	
	// redirect
	location.search = query;
	
};

/*******************************************************************************************

	URL

*******************************************************************************************/

// gets the querystring
// (UNTESTED)
var sbquery = function() {
    return location.search.replace("?", "");
};

// redirects the page
// (UNTESTED)
var sbredirect = function(a) {
	sbstatus("Redirecting...");
    location.search = sbserialize(a);
};

// turns a querystring into an object
var sbdeserialize = function(s) {

    var
    	args = {},
    	pairs = s.split("&");

    for (var i = 0, l = pairs.length; i < l; i++) {

        var pos = pairs[i].indexOf('=');
        if (pos == -1) {
            continue
        };
        var argname = pairs[i].substring(0, pos);
        var value = pairs[i].substring(pos + 1);
        args[argname] = unescape(value);

    };

    return args;

};

// turns an object into a querystring
var sbserialize = function(o) {

    var
    s = "",
    first = true;

    // loop through each property in the object
    for (var property in o) {

        // do we need an &?
        if (first) {
            first = false;
        } else {
            s += "&";
        }

        // add this property
        s += escape(property.toString()) + "=" + escape(o[property]);

    }

    // return the string
    return s;

};


/*******************************************************************************************

	DOM

*******************************************************************************************/

// adds a <script/> tag to the head
var sbscript = function(s) {

    var el = document.createElement("script");
    el.type = "text/javascript";
    el.src = s + "?cachebuster=" + (new Date()).getTime();
    sbhead().appendChild(el);

};

// gets the document <head/> tag
var sbheadcache = null;
var sbhead = function() {
    if (!sbheadcache) {
        sbheadcache = document.getElementsByTagName("head")[0];
    }
    return sbheadcache;
};


// shows an element by ID
var sbshow = function(id) {
    sbget(id).style.display = "block";
};

// sbget - gets an element by id
var sbget = function(id) {
	return document.getElementById(id);
};

// updates the 'value' (innerHTML or .value depending on the element type)
// of an element
// 
// id	-	ID of the element to update
// val	-	Value to insert into the element
var sbval = function(id, val) {

    var el = sbget(id);
	val = val || "&nbsp;";

    if (el.tagName.toLowerCase() == "input") {
    
    	// set the input value
        el.value = val;
        
    } else {
    	
   		// set the inner HTML
       	el.innerHTML = val;
       
    }

};

// sets the status
var sbstatus = function(s) {
	if (s == "") {
		sbget("status").style.display = "none";
	} else {
    	sbval("status", s ? s + "&hellip;" : "");
    }
};

// set the page title
var sbtitle = function(t) {
	//sbval("page-title", t + " - Silobox");
	document.title = t + " - Silobox";
};


/*******************************************************************************************

	BENCHMARKING

*******************************************************************************************/

// schedules a test run (see sbscheduledruntest below)
var sbscheduleruntest = function(sbproject, sbargs) {

	window.setTimeout((function(sbproject, sbargs){
		return function() {
			sbscheduledruntest(sbproject, sbargs);
		}
	})(sbproject, sbargs), sbproject.testdelay);

};


// called when the timeout expires and the test should be run
// (this gets triggered by sbscheduleruntest() above)
var sbscheduledruntest = function(sbproject, sbargs) {

    // run the test and redirect afterwards
    var args = sbruntest(sbproject, sbargs);

    // is there another test to run?
    if (args.s > sbproject.tests.length) {

        // complete - remove the 's' option
        delete args.s;

    }

    // redirect
    sbredirect(args);
	
};

// run the test
var sbruntest = function(o, a) {

    var
	    // test to run
	    test = o.tests[a.s - 1],

	    // running start times
	    start = null,

	    // results collection (an average will be taken)
	    times = [];
	
    // do the iterations
    for (var ii = o.iterations; ii--;) {

        // start the time stamp
        var start = (new Date()).getTime();

        // do the reps
        for (var i = o.reps; i--;) {

            // run the test code
            test.code();

        }

        // save the time it took to complete the reps
        times.push((new Date()).getTime() - start);

    };

    // for calculations
    var total = 0,
    average = 0;

    // get the total
    for (var i = times.length; i--;) {
        total += times[i];
    };

    // work out the average
    average = sbaverage(total, times.length, times);

    // update the arguments
    a.s++;
    a.r = sbaddresult(a.r, average);

    // return the updated arguments
    return a;

};

// calculates the mean average
// t - total
// l - number of items
// d - array of times (ms)
var sbaverage_mean = function(t, l, d) {
	return t / l;
};

// calculates the average
// t - total
// l - number of items
// d - array of times (ms)
var sbaverage = sbaverage_mean;


// adds the result to the string
// r - result string
// t - item to add
var sbaddresult = function(r, t) {
    return r ? r + "," + t.toString() : t.toString();
};



/*******************************************************************************************

	VIEWS

*******************************************************************************************/

// sets up the 'request project' view
var sbshow_request_project = function() {

	// bind to the 'begin' button
	sbget("begin-buttom").onclick = function() {
		
		// next step... jump straight to testing
		sbbegin(true);
		
		// forget about the original form submission
		return false;
		
	};

	// explicitly handle the form submission
	var form = sbget("set-project-form");
	
	// SIMON TODO: Test this in all browsers
	form.onsubmit = function() {
		
		// next step...
		sbbegin();
		
		// forget about the original form submission
		return false;
		
	};
	
	// show the right div
    sbshow("request-project-panel");

};

// shows the project summary
var sbshow_project_summary = function(proj) {

    // clear the status
    sbstatus("");

    // set the project high-level details
    sbval("project-title", proj.title || "");
    sbval("project-description", proj.description || "");
    sbval("project-reps", proj.reps);
    sbval("project-iterations", proj.iterations);
    sbval("project-test-count", proj.tests.length)

	// set the user agent
	sbval("user-agent", "Run on " + BrowserDetect.browser + " " + BrowserDetect.version + " on " + BrowserDetect.OS);

    // show the project summary
    sbshow("project-summary");

};

// shows the project details
var sbshow_project_details = function(proj, args) {

    // TODO: TEST
    // set the hidden variables for the submit form
    sbval("project-hidden-input", args.p);

    // build the test list
    var o = "",
    tests = proj.tests;

    for (var i = 0, l = tests.length; i < l; i++) {

        var test = tests[i];
        o += "<li>"
        + "<h5>" + test.title + "</h5>"
        + "<p class=\"description\">" + test.description + "</p>"
        + "<textarea class=\"code-preview\">" + test.code.toString() + "</textarea>"
        + "</li>";

    };

    sbval("project-test-details", o);

    sbshow("project-details");

};

// shows the results
var sbshow_results = function(proj, args) {
	
	// change "Begin" to "Rerun tests"
	sbval("begin-button", "Run again");
	sbstatus("Building report");
	sbtitle(proj.title);
	
	sbval("reps-count", proj.reps);
	
	// build the results
	
	var table = sbget("results-table-body");
	
	// get the results
	var results = args.r.split(",");
	
	var min = null, max = 0, total = 0, result, perc, revPerc, totalPerc, revTotalPerc, niceTotalPerc, 
		summaryData = "", summaryLabels = "";
		
	for (i = results.length; i--;) {
		
		result = results[i];
		
		// get the max
		max = Math.max(result, max);
		
		if (min == null) {
			min = result;
		}
		
		min = Math.min(result, min);
		
		// sum it up
		total += result;
		
	};
	
	sbval("min-value", sbround(max) + " ms");
	sbval("top-value", sbround(min) + " ms");
	
	// loop through each result
	for (var i = 0, l = results.length; i < l; i++) {
		
		var test = proj.tests[i];
		result = results[i];
		perc = (100 / ((max) / (result)));
		revPerc = 100 - perc;
		nicePerc = (parseInt((revPerc * 100)) / 100);
		
		totalPerc = (100 / ((max - min) / (result - min)));
		revTotalPerc = 100 - totalPerc;
		niceTotalPerc = (parseInt((revTotalPerc * 100)) / 100);

		summaryData += sbround(niceTotalPerc) + ",";
		summaryLabels += (i+1) + "|";

		chartSrc = "http://chart.apis.google.com/chart?chs=180x80&chco=aaaaaa,ff0000&cht=gom&chd=t:" + revPerc;
		
		var row = document.createElement("tr");
		
		var idCol = document.createElement("td");
		idCol.innerHTML = (i + 1);
		idCol.className = "left";

		var titleCol = document.createElement("td");
		titleCol.innerHTML = "<strong>" + test.title + "</strong><p>" + test.description + "</p>";
		titleCol.className = "mid";
		
		//var descriptionCol = document.createElement("td");
		//descriptionCol.innerHTML = test.description;
		
		var msCol = document.createElement("td");
		msCol.innerHTML = "<span title=\"" + result + "\">" + sbround(result) + "</span>";
		msCol.className = "mid";
		var graphCol = document.createElement("td");
		graphCol.innerHTML = "<img src=\"" + chartSrc + "\" />";
		graphCol.className = "right";
		
		// highlight the winner(s)
		if (result == min) {
			row.className = "winner";
		}
		
		row.appendChild(idCol);
		row.appendChild(titleCol);
		//row.appendChild(descriptionCol);
		row.appendChild(msCol);
		row.appendChild(graphCol);
		
		if (result == min) {
			row.cssClass = "winner";
		}
		
		table.appendChild(row);
		
	}
	
	// trim off the last comma
	if (summaryData.length > 1) {
		summaryData = summaryData.substring(0, summaryData.length - 1);
	}

	// add the totals chart
	var totalChart = document.createElement("img");
	totalChart.src = "http://chart.apis.google.com/chart?chxl=1:|" + sbround(max) + " ms||" + sbround(min) + " ms&chxt=x,y&chs=700x200&cht=bvs&chco=ff0000&chd=t:" + summaryData + "&chl=" + summaryLabels;
	
	sbget("results-summary").appendChild(totalChart);

	// clear the status
	sbstatus("");
	
	// set the URL shortner
	sbget("get-short-url-link").href = "http://bit.ly/?url=" + escape(location.href);

	// show the results panel
	sbshow("project-results");
	
};


/*******************************************************************************************

	HELPER METHODS

*******************************************************************************************/

// an empty function for tests to call
function do_something() {
	// in fact, don't do something - just coming to this function is enough
};

// rounds a number to 4 decimal places
function sbround(n) {
	return parseInt((n * 10000)) / 10000;
};

/*

	BROWSER DETECTION
	
*/
var BrowserDetect = {
	init: function () {
		this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
		this.version = this.searchVersion(navigator.userAgent)
			|| this.searchVersion(navigator.appVersion)
			|| "an unknown version";
		this.OS = this.searchString(this.dataOS) || "an unknown OS";
	},
	searchString: function (data) {
		for (var i=0;i<data.length;i++)	{
			var dataString = data[i].string;
			var dataProp = data[i].prop;
			this.versionSearchString = data[i].versionSearch || data[i].identity;
			if (dataString) {
				if (dataString.indexOf(data[i].subString) != -1)
					return data[i].identity;
			}
			else if (dataProp)
				return data[i].identity;
		}
	},
	searchVersion: function (dataString) {
		var index = dataString.indexOf(this.versionSearchString);
		if (index == -1) return;
		return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
	},
	dataBrowser: [
		{
			string: navigator.userAgent,
			subString: "Chrome",
			identity: "Chrome"
		},
		{ 	string: navigator.userAgent,
			subString: "OmniWeb",
			versionSearch: "OmniWeb/",
			identity: "OmniWeb"
		},
		{
			string: navigator.vendor,
			subString: "Apple",
			identity: "Safari",
			versionSearch: "Version"
		},
		{
			prop: window.opera,
			identity: "Opera"
		},
		{
			string: navigator.vendor,
			subString: "iCab",
			identity: "iCab"
		},
		{
			string: navigator.vendor,
			subString: "KDE",
			identity: "Konqueror"
		},
		{
			string: navigator.userAgent,
			subString: "Firefox",
			identity: "Firefox"
		},
		{
			string: navigator.vendor,
			subString: "Camino",
			identity: "Camino"
		},
		{		// for newer Netscapes (6+)
			string: navigator.userAgent,
			subString: "Netscape",
			identity: "Netscape"
		},
		{
			string: navigator.userAgent,
			subString: "MSIE",
			identity: "Microsoft Internet Explorer (IE)",
			versionSearch: "MSIE"
		},
		{
			string: navigator.userAgent,
			subString: "Gecko",
			identity: "Mozilla",
			versionSearch: "rv"
		},
		{ 		// for older Netscapes (4-)
			string: navigator.userAgent,
			subString: "Mozilla",
			identity: "Netscape",
			versionSearch: "Mozilla"
		}
	],
	dataOS : [
		{
			string: navigator.platform,
			subString: "Win",
			identity: "Windows"
		},
		{
			string: navigator.platform,
			subString: "Mac",
			identity: "Mac"
		},
		{
			   string: navigator.userAgent,
			   subString: "iPhone",
			   identity: "iPhone/iPod"
	    },
		{
			string: navigator.platform,
			subString: "Linux",
			identity: "Linux"
		}
	]

};
BrowserDetect.init();

